"""
@File    :   service.py
@Time    :   2024/04/04 14:58:23
@Author  :   Qrj
@Email   :   923207736@qq.com
@description   :   菜单相关业务逻辑
"""

from cashews import NOT_NONE
from sqlalchemy import and_, delete, insert, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from src.business.admin.system.role.models import RoleMenuRel
from src.business.admin.system.tenant.package.models import TenantPackageMenuRel
from src.exceptions.custom_async_validator import generate_async_validator_exception
from src.utils.cashews import CashewsCondition, data_cache

from .models import Menu
from .schemas.out import (
    ListMenuCompleteOutSchema,
    ListMenuSimpleOutSchema,
    MenuCompleteOutSchema,
    MenuSimpleOutSchema,
)
from .schemas.validate import (
    MenuCreateValidateSchema,
    MenuDeleteValidateSchema,
    MenuListSearchValidateSchema,
    MenuUpdateStatusValidateSchema,
    MenuUpdateValidateSchema,
)


class MenuService:
    """菜单业务逻辑类"""

    @data_cache(
        ttl="30m",
        key="menu#id:{menu_id}",
        tags=["menu#id", "menu#id:{menu_id}"],
        condition=NOT_NONE,
    )
    @staticmethod
    async def get_menu_use_id(
        db_session: AsyncSession,
        menu_id: int,
        raise_not_exists_exception: bool = True,
        raise_disabled_exception: bool = True,
    ) -> MenuCompleteOutSchema | None:
        """查找菜单(菜单ID)

        Args:
            db_session (AsyncSession): 数据库连接池
            menu_id (int): 菜单ID
            raise_not_exists_exception (bool, optional): 未找到时是否报错
            raise_disabled_exception (bool, optional): 禁用时是否报错

        Returns:
            MenuCompleteOutSchema | None: 菜单信息
        """
        search_sql = select(
            *MenuCompleteOutSchema.convert_to_sql_model_field(Menu)
        ).where(Menu.id == menu_id)
        async with db_session.begin():
            search_res = await db_session.execute(search_sql)
        result = search_res.mappings().first()
        if result is not None:
            # 转换成pydantic的序列化模型
            result = MenuCompleteOutSchema.model_validate(result)
            if (not result.status) and raise_disabled_exception:
                # 禁用则报错
                raise generate_async_validator_exception("id", "菜单已被禁用")
            return result
        elif result is None and raise_not_exists_exception:
            # 未找到则报错
            raise generate_async_validator_exception("id", "菜单不存在")
        else:
            return result

    @data_cache(
        ttl="30m",
        key="menu#permission:{menu_permission}",
        tags=["menu#permission", "menu#permission:{menu_permission}"],
        condition=NOT_NONE,
    )
    @staticmethod
    async def get_menu_use_permission(
        db_session: AsyncSession,
        menu_permission: str,
        raise_not_exists_exception: bool = True,
        raise_disabled_exception: bool = True,
    ) -> MenuCompleteOutSchema | None:
        """查找菜单(菜单权限标识)

        Args:
            db_session (AsyncSession): 数据库连接池
            menu_permission (str): 菜单权限标识
            raise_not_exists_exception (bool, optional): 未找到时是否报错
            raise_disabled_exception (bool, optional): 被禁用时是否报错

        Returns:
            MenuCompleteOutSchema | None: 菜单信息
        """
        if menu_permission:
            search_sql = select(
                *MenuCompleteOutSchema.convert_to_sql_model_field(Menu)
            ).where(Menu.permission == menu_permission)
            async with db_session.begin():
                search_res = await db_session.execute(search_sql)
            result = search_res.mappings().first()
            if result is not None:
                # 转换成pydantic的序列化模型
                result = MenuCompleteOutSchema.model_validate(result)
                if (not result.status) and raise_disabled_exception:
                    # 禁用则报错
                    raise generate_async_validator_exception(
                        "permission", "菜单已被禁用"
                    )
                return result
            elif result is None and raise_not_exists_exception:
                # 未找到则报错
                raise generate_async_validator_exception("permission", "菜单不存在")
            else:
                return result
        else:
            raise generate_async_validator_exception("permission", "菜单不存在")

    @data_cache(
        ttl="30m",
        key="complete_menu_list:{search_schema}",
        tags=["complete_menu_list"],
        condition=CashewsCondition.condition_only_true_result,
    )
    @staticmethod
    async def list_search_complete_menu(
        db_session: AsyncSession, search_schema: MenuListSearchValidateSchema
    ) -> list[MenuCompleteOutSchema]:
        """完整信息的菜单列表查询

        Args:
            db_session (AsyncSession): 数据库连接池
            search_schema (MenuListSearchValidateSchema): 查询参数

        Returns:
            list[MenuCompleteOutSchema]: 查询结果
        """
        # 构建查询sql
        list_search_sql = select(
            *MenuCompleteOutSchema.convert_to_sql_model_field(Menu)
        ).where(and_(*search_schema.convert_to_where_sql(Menu)))
        # 开始查询
        async with db_session.begin():
            list_search_res = await db_session.execute(list_search_sql)
        result = list_search_res.mappings().all()
        # 统一转为pydantic列表模型
        return ListMenuCompleteOutSchema.validate_python(result)

    @data_cache(
        ttl="30m",
        key="simple_menu_list:{search_schema}",
        tags=["simple_menu_list"],
        condition=CashewsCondition.condition_only_true_result,
    )
    @staticmethod
    async def list_search_simple_menu(
        db_session: AsyncSession, search_schema: MenuListSearchValidateSchema
    ) -> list[MenuSimpleOutSchema]:
        """完整信息的菜单列表查询

        Args:
            db_session (AsyncSession): 数据库连接池
            search_schema (MenuListSearchValidateSchema): 查询参数

        Returns:
            list[MenuSimpleOutSchema]: 查询结果
        """
        # 构建查询sql
        list_search_sql = select(
            *MenuSimpleOutSchema.convert_to_sql_model_field(Menu)
        ).where(and_(*search_schema.convert_to_where_sql(Menu)))
        # 开始查询
        async with db_session.begin():
            list_search_res = await db_session.execute(list_search_sql)
        result = list_search_res.mappings().all()
        # 统一转为pydantic列表模型
        return ListMenuSimpleOutSchema.validate_python(result)

    @staticmethod
    @data_cache.transaction()
    async def create_menu(
        db_session: AsyncSession,
        create_schema: MenuCreateValidateSchema,
        creator_username: str = "",
        is_system: bool = False,
    ) -> int:
        """创建菜单

        Args:
            db_session (AsyncSession): 数据库连接池
            create_schema (MenuCreateValidateSchema): 创建操作所需的数据
            creator_username (str, optional): 创建者用户名
            is_system (bool, optional): 是否系统菜单

        Returns:
            int: 创建的sql的主键ID
        """
        # 验证父级菜单是否存在
        await MenuService.verify_parent_menu_exists(db_session, create_schema.parent_id)
        # 构建创建所需的数据
        create_schema_data = create_schema.model_dump()
        # 创建sql
        create_sql = insert(Menu).values(
            **create_schema_data, creator_username=creator_username, is_system=is_system
        )
        # 创建操作
        async with db_session.begin():
            # 创建sql数据
            create_res = await db_session.execute(create_sql)
            # 清空相关缓存标签
            await data_cache.delete_tags("complete_menu_list", "simple_menu_list")
        return create_res.inserted_primary_key_rows[0][0]

    @staticmethod
    @data_cache.transaction()
    async def update_menu(
        db_session: AsyncSession,
        update_schema: MenuUpdateValidateSchema,
        updater_username: str = "",
    ):
        """修改菜单

        Args:
            db_session (AsyncSession): 数据库连接池
            update_schema (MenuUpdateValidateSchema): 修改操作所需的数据
            updater_username (str, optional): 修改者用户名
        """
        # 检查需修改的sql实例是否存在(禁用不报错)
        update_sql_obj = await MenuService.get_menu_use_id(
            db_session, update_schema.id, raise_disabled_exception=False
        )
        # 验证父级菜单是否存在
        await MenuService.verify_parent_menu_exists(db_session, update_schema.parent_id)
        # 检查是否修改了系统级别菜单
        if update_sql_obj.is_system:
            raise generate_async_validator_exception("id", "系统级别菜单不允许修改")
        # 构建修改所需的数据(排除未设置的值和id)
        update_schema_data = update_schema.model_dump(
            exclude=("id",), exclude_unset=True
        )
        if update_schema_data:
            # 修改sql
            update_sql = (
                update(Menu)
                .values(**update_schema_data, updater_username=updater_username)
                .where(Menu.id == update_schema.id)
            )
            # 修改操作
            async with db_session.begin():
                # 修改sql数据
                await db_session.execute(update_sql)
                # 清空相关缓存标签
                # 当修改前后权限标识不为空时, 清空修改前和修改后的菜单权限标识缓存
                menu_permission_cache_name_list = []
                if update_sql_obj.permission:
                    menu_permission_cache_name_list.append(
                        f"menu#permission:{update_sql_obj.permission}"
                    )
                if update_schema.permission:
                    menu_permission_cache_name_list.append(
                        f"menu#permission:{update_schema.permission}",
                    )
                await data_cache.delete_tags(
                    "complete_menu_list",
                    "simple_menu_list",
                    "role_menu_info",  # 角色关联的菜单信息缓存
                    "tenant_package_all_menu_list",  # 所有租户套餐的所有菜单信息
                    f"menu#id:{update_schema.id}",
                    *menu_permission_cache_name_list,
                )

    @staticmethod
    async def update_menu_status(
        db_session: AsyncSession,
        update_schema: MenuUpdateStatusValidateSchema,
        updater_username: str = "",
    ):
        """单独修改菜单状态

        Args:
            db_session (AsyncSession): 数据库连接池
            update_schema (MenuUpdateStatusValidateSchema): 修改操作所需的数据
            updater_username (str, optional): 修改者用户名
        """
        # 检查需修改的sql实例是否存在(禁用不报错)
        update_sql_obj = await MenuService.get_menu_use_id(
            db_session, update_schema.id, raise_disabled_exception=False
        )
        # 检查是否修改了系统级别菜单
        if update_sql_obj.is_system:
            raise generate_async_validator_exception("id", "系统级别菜单不允许修改")
        # 修改sql
        update_sql = (
            update(Menu)
            .values(status=update_schema.status, updater_username=updater_username)
            .where(Menu.id == update_schema.id)
        )
        # 修改操作
        async with db_session.begin():
            # 修改sql数据
            await db_session.execute(update_sql)
            # 当修改前权限标识不为空时, 清空修改前的菜单权限标识缓存
            menu_permission_cache_name_list = []
            if update_sql_obj.permission:
                menu_permission_cache_name_list.append(
                    f"menu#permission:{update_sql_obj.permission}"
                )
            # 清空相关缓存标签
            await data_cache.delete_tags(
                "complete_menu_list",
                "simple_menu_list",
                "role_menu_info",  # 角色关联的菜单信息缓存
                "tenant_package_all_menu_list",  # 所有租户套餐的所有菜单信息
                f"menu#id:{update_schema.id}",
                *menu_permission_cache_name_list,
            )

    @staticmethod
    @data_cache.transaction()
    async def delete_menu(
        db_session: AsyncSession, delete_schema: MenuDeleteValidateSchema
    ):
        """删除菜单(需要同时删除menuPermission表对应的menu_id数据)

        Args:
            db_session (AsyncSession): 数据库连接池
            delete_schema (MenuDeleteValidateSchema): 删除操作所属的数据
        """
        # 验证需删除的菜单是否是父级菜单
        # 构建查询sql
        verify_sql = select(Menu.name).where(Menu.parent_id.in_(delete_schema.ids))
        # 进行sql查询
        async with db_session.begin():
            verify_res = await db_session.execute(verify_sql)
        verify_result = verify_res.scalar()
        if verify_result:
            # 存在父级菜单直接报错
            raise generate_async_validator_exception(
                "ids",
                f"{verify_result}下存在子菜单, 需先删除其所有子菜单后才能进行该菜单的删除操作",
            )

        # 查找需删除的菜单
        # 构建查询sql
        select_sql = select(Menu.id, Menu.permission, Menu.is_system).where(
            Menu.id.in_(delete_schema.ids)
        )
        # 进行sql查询
        async with db_session.begin():
            select_res = await db_session.execute(select_sql)
        select_result = select_res.mappings().all()
        # 检查是否删除了系统级别菜单
        if any([i["is_system"] for i in select_result]):
            raise generate_async_validator_exception("id", "系统级别菜单不允许删除")

        # 进行删除操作
        if select_result:
            # 获取需要删除的菜单id列表
            delete_menu_id_list = [i["id"] for i in select_result]
            # 获取需要删除的菜单权限标识列表(有权限标识的才需要删)
            delete_menu_permission_list = [
                i["permission"] for i in select_result if i["permission"]
            ]
            # 删除菜单本身的sql
            delete_sql = delete(Menu).where(Menu.id.in_(delete_menu_id_list))
            # 删除角色的相关菜单权限sql
            delete_role_permission_rel_sql = delete(RoleMenuRel).where(
                RoleMenuRel.menu_id.in_(delete_menu_id_list)
            )
            # 删除租户套餐相关联菜单sql
            delete_tenant_package_sql = delete(TenantPackageMenuRel).where(
                TenantPackageMenuRel.menu_id.in_(delete_menu_id_list)
            )
            # 删除操作
            async with db_session.begin():
                # 删除sql中的数据
                await db_session.execute(delete_sql)
                await db_session.execute(delete_role_permission_rel_sql)
                await db_session.execute(delete_tenant_package_sql)
                # 清空相关缓存标签
                menu_id_cache_list = [f"menu#id:{i}" for i in delete_menu_id_list]
                menu_permission_cache_list = [
                    f"menu#permission:{i}" for i in delete_menu_permission_list
                ]
                await data_cache.delete_tags(
                    "complete_menu_list",  # 完整的菜单缓存列表
                    "simple_menu_list",  # 简单的菜单缓存列表
                    *menu_id_cache_list,  # 相关菜单ID缓存信息
                    *menu_permission_cache_list,  # 相关菜单权限标识缓存信息
                    "role_menu_info",  # 角色关联的菜单信息缓存
                    "tenant_package_all_menu_list",  # 所有租户套餐的所有菜单信息
                )

    @staticmethod
    async def verify_parent_menu_exists(db_session: AsyncSession, menu_parent_id: int):
        """验证菜单的父级菜单是否存在

        Args:
            db_session (AsyncSession): 数据库连接池
            menu_parent_id (int): 父级菜单ID
        """
        # 当父级菜单ID为0时表示没有父级菜单
        if menu_parent_id != 0:
            # 非0时, 验证父级菜单是否存在
            parent_menu = await MenuService.get_menu_use_id(
                db_session,
                menu_parent_id,
                raise_disabled_exception=False,
                raise_not_exists_exception=False,
            )
            if parent_menu is None:
                raise generate_async_validator_exception(
                    "parent_id", "选择的父级菜单不存在"
                )
